﻿/*  This file is part of TextEditorLibrary - a library for the programmatic manipulation of text.

    TextEditorLibrary is free software: you can redistribute it and/or modify it under the terms
    of the GNU Lesser General Public License as published by the Free Software Foundation, either
    version 3 of the License, or (at your option) any later version.

    TextEditorLibrary is distributed in the hope that it will be useful, but WITHOUT ANY 
    WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
    PURPOSE.  See the GNU Lesser General Public License for more details at 
    <http://www.gnu.org/licenses/>.
*/

using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Forms;

namespace TextEditorLibrary
{
 
/// <summary><para>Defines a rich text box for use with a Text, inheriting from System.Windows.Forms.RichTextBox.</para>
/// <para>Box and Text can change independently via code or user interaction; events are raised when either happens. 
/// When the two are synchronised, the user selection in the Box can be mapped into a Range, and a Range can be
/// used to make a selection in the Box.</para>
/// <para>The Box is ReadOnly by default and uses default font Consolas 9pt.</para></summary>
    public class Box : RichTextBox
    {
        private Range _range;       // Associated Range
        private bool _dirtyBox;     // Flag when text box contents have changed (and no longer match the Range)
        private bool _dirtyRange;   // Flag when Range contents have changed (and no longer match the text box)
        private bool _working;      // So we know that we are saving or loading and a change to Box or Range does not trigger an event
        private string _content;    // Save plain text contents so we know if it gets dirty

    /// <summary>Occurs when the source Range for the Box is first changed in code.</summary>
        public event EventHandler RangeDirty;
    /// <summary>Occurs when the plain text in the Box is first changed by the user or calling code.</summary>
        public event EventHandler BoxDirty;

    /// <summary>Creates a rich text box to be associated with a Range.</summary>
        public Box()
        {
            base.ReadOnly = true;   // ReadOnly by default - have to take care when Text and Box change independently
            base.Font = new System.Drawing.Font("Consolas", 9.0f);  // Default fixed pitch font
            _range = null;
            _dirtyBox = false;
            _dirtyRange = false;
            _working = false;
            _content = "";
        }

    // Catch the TextChanged event so we can mark the box dirty if the text has changed, either due to manual
    // entry or code.
        protected override void OnTextChanged(EventArgs e)
        {
        // Not if we are loading or reloading, or the box is already dirty
            if (!_working && !_dirtyBox)
            {
            // Efficiency - most text changes will change the length and presumably this is O(1)
                if (base.TextLength != _content.Length) _dirtyBox = true;
                else if (base.Text != _content) _dirtyBox = true;

                if (_dirtyBox && BoxDirty != null) BoxDirty(this, EventArgs.Empty);
            }
            base.OnTextChanged(e);
        }

    // Called by the Text to catch the first change to the contents of the source Range and raise event.
        internal void OnRangeDirty()
        {

        // Not if we are saving, or the range is already dirty
            if (!_working && !_dirtyRange)
            {
                _dirtyRange = true;
                if (RangeDirty != null) RangeDirty(this, EventArgs.Empty);
            }
        }

    /// <summary>Replaces the contents of the rich text box with the contents of the Range.</summary>
    /// <param name="range">Range from which to fill the text box.</param>
    /// <exception cref="ArgumentException">The passed Range has been invalidated.</exception>
        public void LoadRange(Range range)
        {
            range.Validate("range");

        // Lose the old Range if we had one, and the Box from the Text's list
            if (_range != null)
            {
                _range.Text.Remove(this);
                _range.Remove();
            }

        // Register the new Range and add the Box
            _range = range.Text.AddRange(range);
            range.Text.AddBox(this);

            LoadRange();
        }

    /// <summary><para>Refreshes the contents of the rich text box with the contents of its Range.</para>
    /// <para>All formatting is lost.</para></summary>
    /// <exception cref="InvalidOperationException">This Box is not associated with a source Range.</exception>
        public void LoadRange()
        {
            if (_range == null)
                throw new InvalidOperationException("This Box is not associated with a source Range.");

        // Need to remember that we are loading, or the TextChanged event will trigger a BoxDirty event
            try
            {
                _working = true;
                base.Text = "";
                base.Text = _range.ToString();
            }
            finally
            {
                _working = false;
            }

            base.SelectionStart = 0;
            base.SelectionLength = 0;
            _dirtyBox = false;
            _dirtyRange = false;
            _content = base.Text;
        }

    /// <summary>Saves the contents of the rich text box into its source Range.</summary>
    /// <exception cref="InvalidOperationException">This Box is not associated with a source Range.</exception>
        public void Save()
        {
            if (_range == null) 
                throw new InvalidOperationException("This Box is not associated with a source Range.");

        // We have to remember that we are saving, otherwise the changing Range will trigger a RangeDirty event
            try
            {
                _working = true;
                _range.Replace(base.Text);
            }
            finally
            {
                _working = false;
            }
            _dirtyBox = false;
            _dirtyRange = false;
        }

    /// <summary>Selects the text which is shared by the rich text box and the passed Range.</summary>
    /// <param name="range">Range defining the new selection.</param>
    /// <exception cref="InvalidOperationException">This Box is not associated with a source Range, or
    /// its contents are not synchronised with the source Range.</exception>
    /// <exception cref="ArgumentException">The passed Range has been invalidated or is in a
    /// different Text.</exception>
        public void SelectRange(Range range)
        {
            if (_range == null) 
                throw new InvalidOperationException("This Box is not associated with a source Range.");
            if (_dirtyBox || _dirtyRange)
                throw new InvalidOperationException("Box contents are not synchronised with source Range contents.");
            range.Validate(_range.Text, "range");

            long start = range._mark1.Offset(_range._mark1);
            if (start<0) start = 0;

            long end = _range._mark2.Offset(range._mark2);
            if (end<0) end = 0;

        // We know that positions in the box must be within the 32-bit limit
            base.Select((int)start, (int)(base.TextLength-start-end));
        }

    /// <summary><para>Maps the text selected by the user into an unregistered Range in the source Text.</para>
    /// <para>Do not assign this value to a Range variable - it will become invalidated if the
    /// text changes.  Use Text.AddRange() or Range.MoveTo() instead.</para></summary>
    /// <exception cref="InvalidOperationException">This Box is not associated with a source Range, or
    /// its contents are not synchronised with the source Range.</exception>
        public Range SelectedRange()
        {
            if (_range == null) 
                throw new InvalidOperationException("This Box is not associated with a source Range.");
            if (_dirtyBox || _dirtyRange)
                throw new InvalidOperationException("Box contents are not synchronised with source Range contents.");

            int start = base.SelectionStart;
            int end = start + base.SelectionLength;

            return new Range(_range.Start.OffsetMark(start,true), _range.Start.OffsetMark(end,true));
        }

    /// <summary>Gets an unregistered Range matching the source Range for the Box or null.</summary>
        public Range Range
        {
            get
            {
                if (_range == null) return _range;
                else return new Range(_range.Start, _range.End);
            }
        }

    /// <summary>True if the contents of the Box may no longer match the contents of the Range.</summary>
        public bool Dirty
        {
            get { return _dirtyBox || _dirtyRange; }
        }

    /// <summary>True if the contents of the Box have changed since the last Save or LoadRange.</summary>
        public bool DirtyBox
        {
            get { return _dirtyBox; }
        }

    /// <summary>True if the contents of the Range have changed since the last Save or LoadRange.</summary>
        public bool DirtyRange
        {
            get { return _dirtyRange; }
        }

    }
}
